﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;

namespace Ewk.Runtime.Serialization.PositionBased
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = true)]
    public sealed class PositionBasedDatarecordAttribute : DatarecordAttribute
    {
        private const char _spaceCharacter = ' ';

        public PositionBasedDatarecordAttribute(int length)
        {
            if (length <= 0) throw new ArgumentException(ExceptionMessages.InvalidLengthValue, "length");

            Length = length;
            DefaultCharacter = _spaceCharacter;
            LengthCriterium = DatarecordLengthCriterium.EqualsDefinition;
        }

        /// <summary>
        /// Gets the length of the datarecord represented by the <see cref="Type"/> this <see cref="Attribute"/> was specified on.
        /// </summary>
        public int Length { get; private set; }

        /// <summary>
        /// Gets or sets the criterium for the length of a datarecord.
        /// </summary>
        public DatarecordLengthCriterium LengthCriterium { get; set; }

        /// <summary>
        /// Gets or sets the default character for the <see cref="string"/> representation of the datarecord.
        /// This character is used for empty spaces within the <see cref="PositionBasedDatarecordAttribute"/>.
        /// </summary>
        public char DefaultCharacter { get; set; }

        protected internal override IEnumerable<DatafieldAttribute> PrepareDatafieldAttributes(IEnumerable<DatafieldAttribute> datafieldAttributes)
        {
            if (datafieldAttributes == null) throw new ArgumentNullException("datafieldAttributes");

            // Validate the Field Types. They must all be of the same Type.
            if (datafieldAttributes.Any(attribute => attribute.GetType() != typeof (PositionBasedDatafieldAttribute)))
            {
                var exceptionMessage = string.Format(CultureInfo.CurrentCulture, ExceptionMessages.UnexpectedDatafieldAttribute, typeof(PositionBasedDatafieldAttribute));
                throw new DatafieldTypeException(exceptionMessage);
            }

            // Check overlapping
            var attributeList = datafieldAttributes.Cast<PositionBasedDatafieldAttribute>().ToList();
            attributeList.Sort((a, b) => a.Start.CompareTo(b.Start));

            var position = 0;
            foreach (var attribute in attributeList)
            {
                if (attribute.Start < position)
                {
                    throw new DatafieldOverlapException(ExceptionMessages.DatafieldOverlap);
                }

                position = attribute.Start + attribute.Length * attribute.Occurrence;
            }

            // Check sum of fieldlengths with recordlength
            if (position > Length)
            {
                throw new DatafieldOverflowException(ExceptionMessages.DatafieldOverflow);
            }

            return attributeList;
        }

        protected internal override string Serialize<T>(T datarecord, IEnumerable<DatafieldAttribute> datafieldAttributes)
        {
            if (datafieldAttributes == null) throw new ArgumentNullException("datafieldAttributes");

            var result = new string(DefaultCharacter, Length);

            foreach (var datafieldAttribute in datafieldAttributes)
            {
                var attribute = (PositionBasedDatafieldAttribute)datafieldAttribute;
                // Get the typed attribute and if the Default character has not been set, set it with the DefaultCharacter value of the DatarecordAttribute.
                if (attribute.DefaultCharacter.Equals(default(char)))
                {
                    attribute.DefaultCharacter = DefaultCharacter;
                }

                // Serialze the individual field
                var fieldResult = attribute.Serialize<T>(datarecord);

                result = result.Remove(attribute.Start, attribute.Length * attribute.Occurrence);
                result = result.Insert(attribute.Start, fieldResult);
            }

            return result;
        }

        protected internal override T Deserialize<T>(string datarecord, IEnumerable<DatafieldAttribute> datafieldAttributes)
        {
            if (datarecord == null) throw new ArgumentNullException("datarecord");
            if (datafieldAttributes == null) throw new ArgumentNullException("datafieldAttributes");

            if ((LengthCriterium & DatarecordLengthCriterium.SmallerThanDefinition) != DatarecordLengthCriterium.SmallerThanDefinition && datarecord.Length < Length)
            {
                OnDatarecordIncomplete(this, ExceptionMessages.DatarecordIncomplete);
            }

            if ((LengthCriterium & DatarecordLengthCriterium.GreaterThanDefinition) != DatarecordLengthCriterium.GreaterThanDefinition && datarecord.Length > Length)
            {
                OnDatarecordOverflow(this, ExceptionMessages.DatarecordOverflow);
            }

            var datarecordInstance = (T)typeof(T).Assembly.CreateInstance(typeof(T).FullName);

            foreach (var datafieldAttribute in datafieldAttributes)
            {
                var attribute = (PositionBasedDatafieldAttribute)datafieldAttribute;
                // Get the typed attribute and if the Default character has not been set, set it with the DefaultCharacter value of the DatarecordAttribute.
                if (attribute.DefaultCharacter.Equals(default(char)))
                {
                    attribute.DefaultCharacter = DefaultCharacter;
                }

                // Deserialze the individual field
                var fieldResult = attribute.Deserialize(datarecord);
                attribute.PropertyInfo.SetValue(datarecordInstance, fieldResult, null);
            }

            return datarecordInstance;
        }
    }
}
